﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Reflection;

namespace Konves.Csv.Serialization
{
    /// <summary>
    /// Serializes and deserializes objects into and from CSV documents.
    /// The <see cref="CsvSerializer"/> enables you to control how objects are encoded into CSV.
    /// </summary>
    public class CsvSerializer
    {
        #region == Constructor ==
        /// <summary>
        /// Initializes a new instance of the <see cref="T:Konves.Csv.Serialization.CsvSerializer"/> class that
        /// can serialize objects of the specified type into CSV documents, and
        /// deserialize CSV documents into objects of the specified type.
        /// </summary>
        /// <param name="type">The type of the objects that this <see cref="CsvSerializer"/> can serialize.</param>
        public CsvSerializer(Type type)
            : this(type, true, defaultDelimiter, defaultEscape, CsvEscapeOptions.AutoEscape) { }
        public CsvSerializer(Type type, char delimiter)
            : this(type, true, delimiter, defaultEscape, CsvEscapeOptions.AutoEscape) { }
        public CsvSerializer(Type type, char delimiter, char escapeCharacter)
            : this(type, true, delimiter, escapeCharacter, CsvEscapeOptions.AutoEscape) { }
        public CsvSerializer(Type type, bool includeHeader)
            : this(type, includeHeader, defaultDelimiter, defaultEscape, CsvEscapeOptions.AutoEscape) { }
        public CsvSerializer(Type type, bool includeHeader, char delimiter)
            : this(type, includeHeader, delimiter, defaultEscape, CsvEscapeOptions.AutoEscape) { }
        public CsvSerializer(Type type, bool includeHeader, char delimiter, char escapeCharacter)
            : this(type, includeHeader, delimiter, escapeCharacter, CsvEscapeOptions.AutoEscape) { }
        public CsvSerializer(Type type, CsvEscapeOptions options)
            : this(type, true, defaultDelimiter, defaultEscape, options) { }
        public CsvSerializer(Type type, char delimiter, CsvEscapeOptions options)
            : this(type, true, delimiter, defaultEscape, options) { }
        public CsvSerializer(Type type, char delimiter, char escapeCharacter, CsvEscapeOptions options)
            : this(type, true, delimiter, escapeCharacter, options) { }
        public CsvSerializer(Type type, bool includeHeader, char delimiter, CsvEscapeOptions options)
            : this(type, true, delimiter, defaultEscape, options) { }
        public CsvSerializer(Type type, bool includeHeader, char delimiter, char escapeCharacter , CsvEscapeOptions options)
        {
            this.Type = type;
            this.IncludeHeader = includeHeader;
            this.Delimiter = delimiter;
            this.EscapeCharacter = escapeCharacter;
            this.EscapeOptions = options;

            _fields = this.GetCsvFields(type).ToArray();
        } 
        #endregion

        private const char defaultDelimiter = ',';
        private const char defaultEscape = '"';
        private CsvFieldInfo[] _fields;

        public Type Type { get; private set; }
        public bool IncludeHeader { get; private set; }
        public char Delimiter { get; private set; }
        public char EscapeCharacter { get; private set; }
        public CsvEscapeOptions EscapeOptions { get; private set; }


        #region Deserialize
        public object[] Deserialize(CsvStreamReader csvStreamReader)
        {
            return _Deserialize(csvStreamReader).ToArray();
        }
        public object[] Deserialize(Stream stream)
        {
            return _Deserialize(new CsvStreamReader(stream, this.Delimiter, this.EscapeCharacter)).ToArray();
        }
        private IEnumerable<object> _Deserialize(CsvStreamReader reader)
        {
            string[] headers = null;
            if (this.IncludeHeader)
                headers = reader.ReadRecord();

            while (!reader.EndOfStream)
            {
                object result = Activator.CreateInstance(this.Type);

                // for performance, data could be defined outside the while-loop but still set here;
                // however, that would not allow for jagged csv files.
                string[] data = reader.ReadRecord();

                foreach (CsvFieldInfo fi in this._fields)
                {
                    if (fi.UseIndex && fi.FieldIndex < data.Length)
                    {
                        fi.PropertyInfo.SetValue(result, GetValue(data[fi.FieldIndex], fi.PropertyInfo.PropertyType), null);
                    }
                    else if (!fi.UseIndex && this.IncludeHeader)
                    {
                        int index = Array.IndexOf(headers, fi.FieldName);

                        object value = GetValue(data[index], fi.PropertyInfo.PropertyType);

                        fi.PropertyInfo.SetValue(result, value, null);
                    }
                }

                yield return result;
            }
        } 
        #endregion

        #region GetValue
        /// <summary>
        /// Parses the value as the specified type.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private object GetValue(string value, Type type)
        {
            TypeCode c = Type.GetTypeCode(type);

            switch (c)
            {
                case TypeCode.Boolean:
                    bool boolres = default(bool);
                    if (bool.TryParse(value, out boolres))
                        return boolres;
                    break;
                case TypeCode.Byte:
                    byte byteres = default(byte);
                    if (byte.TryParse(value, out byteres))
                        return byteres;
                    break;
                case TypeCode.Char:
                    char charres = default(char);
                    if (char.TryParse(value, out charres))
                        return charres;
                    break;
                case TypeCode.DBNull:
                    break;
                case TypeCode.DateTime:
                    DateTime dateres = default(DateTime);
                    if (DateTime.TryParse(value, out dateres))
                        return dateres;
                    break;
                case TypeCode.Decimal:
                    decimal decres = default(decimal);
                    if (decimal.TryParse(value, out decres))
                        return decres;
                    break;
                case TypeCode.Double:
                    double dubres = default(double);
                    if (double.TryParse(value, out dubres))
                        return dubres;
                    break;
                case TypeCode.Empty:
                    break;
                case TypeCode.Int16:
                    Int16 int16res = default(Int16);
                    if (Int16.TryParse(value, out int16res))
                        return int16res;
                    break;
                case TypeCode.Int32:
                    Int32 int32res = default(Int32);
                    if (Int32.TryParse(value, out int32res))
                        return int32res;
                    break;
                case TypeCode.Int64:
                    Int64 int64res = default(Int64);
                    if (Int64.TryParse(value, out int64res))
                        return int64res;
                    break;
                case TypeCode.Object:
                    return value;
                case TypeCode.SByte:
                    sbyte sbyteres = default(sbyte);
                    if (sbyte.TryParse(value, out sbyteres))
                        return sbyteres;
                    break;
                case TypeCode.Single:
                    Single singleres = default(Single);
                    if (Single.TryParse(value, out singleres))
                        return singleres;
                    break;
                case TypeCode.String:
                    return value;
                case TypeCode.UInt16:
                    UInt16 uint16res = default(UInt16);
                    if (UInt16.TryParse(value, out uint16res))
                        return uint16res;
                    break;
                case TypeCode.UInt32:
                    UInt32 uint32res = default(UInt32);
                    if (UInt32.TryParse(value, out uint32res))
                        return uint32res;
                    break;
                case TypeCode.UInt64:
                    UInt64 uint64res = default(UInt64);
                    if (UInt64.TryParse(value, out uint64res))
                        return uint64res;
                    break;
                default:
                    break;
            }

            throw new FormatException(string.Format("Unable to parse {0} as {1}.", value, type.FullName));
        } 
        #endregion

        #region Serialize
        /// <summary>
        /// Serializes the specified Objects and writes the CSV document to a file using the specified <see cref="TextWriter"/>.
        /// </summary>
        /// <param name="textWriter">The <see cref="TextWriter"/> used to write the CSV document.</param>
        /// <param name="objs">The objects to serialize.</param>
        public void Serialize(TextWriter textWriter, object[] objs)
        {
            string t = string.Empty;

            string singleEscape = new string(this.EscapeCharacter, 1);
            string doubleEscape = new string(this.EscapeCharacter, 2);

            for (int o = 0; o < objs.Length;o++ )
            {
                for (int f = 0; f < _fields.Length; f++)
                {
                    switch (this.EscapeOptions)
                    {
                        case CsvEscapeOptions.AlwaysEscape:
                            textWriter.Write(this.EscapeCharacter);
                            textWriter.Write(
                                _fields[f].PropertyInfo.GetValue(objs[o], null).ToString()
                                .Replace(singleEscape, doubleEscape));
                            textWriter.Write(this.EscapeCharacter);
                            break;
                        case CsvEscapeOptions.AutoEscape:
                            string value = _fields[f].PropertyInfo.GetValue(objs[o], null).ToString();
                            if (value.Contains(this.EscapeCharacter) || value.Contains(this.Delimiter))
                            {
                                textWriter.Write(this.EscapeCharacter);
                                textWriter.Write(value.Replace(singleEscape, doubleEscape));
                                textWriter.Write(this.EscapeCharacter);
                            }
                            else
                            {
                                textWriter.Write(_fields[f].PropertyInfo.GetValue(objs[o], null).ToString());
                            }
                            break;
                        case CsvEscapeOptions.NeverEscape:
                            textWriter.Write(_fields[f].PropertyInfo.GetValue(objs[o], null).ToString());
                            break;
                        default:
                            break;
                    }

                    // write delimiter unless on last field
                    if (f < _fields.Length - 1)
                        textWriter.Write(this.Delimiter);
                }

                // write linebreak unless on last field
                if (0 < objs.Length - 1)
                    textWriter.Write(Environment.NewLine);
            }
        }
        public void Serialize(StringBuilder stringBuilder, object[] objs)
        {
            string field = "field";

            string t = string.Empty;

            string singleEscape = new string(this.EscapeCharacter, 1);
            string doubleEscape = new string(this.EscapeCharacter, 2);

            if (this.IncludeHeader)
            {
                for (int f = 0; f < _fields.Length; f++)
                {
                    switch (this.EscapeOptions)
                    {
                        case CsvEscapeOptions.AlwaysEscape:
                            stringBuilder.Append(this.EscapeCharacter);
                            if (string.IsNullOrWhiteSpace(_fields[f].FieldName))
                            {
                                stringBuilder.Append(field);
                                stringBuilder.Append(_fields[f].FieldIndex);
                            }
                            else
                                stringBuilder.Append(_fields[f].FieldName.Replace(singleEscape, doubleEscape));
                            stringBuilder.Append(this.EscapeCharacter);
                            break;
                        case CsvEscapeOptions.AutoEscape:
                            string value = string.Empty;
                            if (string.IsNullOrWhiteSpace(_fields[f].FieldName))
                            {
                                value = field + _fields[f].FieldIndex;
                            }
                            else
                                value = _fields[f].FieldName;

                            if (value.Contains(this.EscapeCharacter) || value.Contains(this.Delimiter))
                            {
                                stringBuilder.Append(this.EscapeCharacter);
                                stringBuilder.Append(value.Replace(singleEscape, doubleEscape));
                                stringBuilder.Append(this.EscapeCharacter);
                            }
                            else
                            {
                                stringBuilder.Append(value);
                            }
                            break;
                        case CsvEscapeOptions.NeverEscape:
                            if (string.IsNullOrWhiteSpace(_fields[f].FieldName))
                            {
                                stringBuilder.Append(field);
                                stringBuilder.Append(_fields[f].FieldIndex);
                            }
                            else
                                stringBuilder.Append(_fields[f].FieldName);
                            break;
                        default:
                            break;
                    }

                    // write delimiter unless on last field
                    if (f < _fields.Length - 1)
                        stringBuilder.Append(this.Delimiter);
                }

                // write linebreak unless there is no data
                if (objs.Length != 0)
                    stringBuilder.Append(Environment.NewLine);
            }

            for (int o = 0; o < objs.Length; o++)
            {
                for (int f = 0; f < _fields.Length; f++)
                {switch (this.EscapeOptions)
                    {
                    
                        case CsvEscapeOptions.AlwaysEscape:
                            stringBuilder.Append(this.EscapeCharacter);
                            stringBuilder.Append(
                                _fields[f].PropertyInfo.GetValue(objs[o], null).ToString()
                                .Replace(singleEscape, doubleEscape));
                            stringBuilder.Append(this.EscapeCharacter);
                            break;
                        case CsvEscapeOptions.AutoEscape:
                            string value = _fields[f].PropertyInfo.GetValue(objs[o], null).ToString();
                            if (value.Contains(this.EscapeCharacter) || value.Contains(this.Delimiter))
                            {
                                stringBuilder.Append(this.EscapeCharacter);
                                stringBuilder.Append(value.Replace(singleEscape, doubleEscape));
                                stringBuilder.Append(this.EscapeCharacter);
                            }
                            else
                            {
                                stringBuilder.Append(value);
                            }
                            break;
                        case CsvEscapeOptions.NeverEscape:
                            stringBuilder.Append(_fields[f].PropertyInfo.GetValue(objs[o], null).ToString());
                            break;
                        default:
                            break;
                    }

                    // write delimiter unless on last field
                    if (f < _fields.Length - 1)
                        stringBuilder.Append(this.Delimiter);
                }

                // write linebreak unless on last line
                if (0 < objs.Length - 1)
                    stringBuilder.Append(Environment.NewLine);
            }
        }
        #endregion

        #region GetCsvFields
        /// <summary>
        /// Returns all of the CSV field info of the specified type.
        /// </summary>
        /// <param name="type">The type.</param>
        private IEnumerable<CsvFieldInfo> GetCsvFields(Type type)
        {
            foreach (PropertyInfo pi in type.GetProperties())
            {
                CsvAttribute att = (CsvAttribute)pi.GetCustomAttributes(false).Where(a => a.GetType().IsSubclassOf(typeof(CsvAttribute))).FirstOrDefault();

                if (att == null)
                {
                    yield return new CsvFieldInfo(pi, pi.Name, -1, CsvFieldInfo.DefaultFormat);
                }
                else if (!att.Ignore && typeof(ICsvSerializationInfo).IsAssignableFrom(att.GetType()))
                {
                    ICsvSerializationInfo info = att as ICsvSerializationInfo;
                    if (info.UseIndex)
                        yield return new CsvFieldInfo(pi, info.FieldName, info.FieldIndex, info.Format);
                    else
                        yield return new CsvFieldInfo(pi, info.FieldName, -1, info.Format);
                }
            }
        }         
        #endregion
    }

    
}
